﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using FastReflectionLib;
using Fuse8.DomainFramework.Practices.DataAccess.SqlRepository;
using Fuse8.DomainFramework.Practices.Domain.Interfaces;
using Fuse8.DomainFramework.Practices.SystemFacade.Extensions;

namespace Fuse8.DomainFramework.Practices.SystemFacade
{
	public static class Mappers<T, TEntity> where T : class where TEntity : class
	{
        private readonly static ConcurrentDictionary<string, Func<IDataRecord, TEntity, T>> MapperDelegates = new ConcurrentDictionary<string, Func<IDataRecord, TEntity, T>>();
        private readonly static ConcurrentDictionary<string, Func<IDataRecord, TEntity, T>> MultiMapperDelegates = new ConcurrentDictionary<string, Func<IDataRecord, TEntity, T>>();

		private static readonly Dictionary<Type, MethodInfo> GetMethods = typeof (IDataRecord).GetMethods()
			.Where(x => x.Name.StartsWith("Get") &&
			            !x.Name.EndsWith("Name") &&
			            x.GetParameters().Select(y => y.ParameterType).SequenceEqual(new[] {typeof (int)}))
			.ToDictionary(x => x.ReturnType, x => x);

		static Mappers()
		{
			GetMethods.Add(typeof (byte[]), ExpressionUtility<IDataRecord>.GetMethodInfo(x => x.GetValue(0)));
		}


		private static Func<IDataRecord, TEntity, T> Get(string key, IDataRecord record)
		{
		    return MapperDelegates.GetOrAdd(key, e => CreateMapperDelegate(key, record, typeof (T)));
		}

		public static T Get(string key, IDataRecord record, TEntity stepEntity)
		{
			return Get(key, record)(record, stepEntity);
		}

		private static Func<IDataRecord, TEntity, T> CreateMapperDelegate(string key, IDataRecord record, Type returnType)
		{
			var dm = new DynamicMethod(String.Format("ʬʬMapperDelegates_{0}", key), returnType, new[] { typeof(IDataRecord), typeof(TEntity) }, typeof(EntityGenerator));
			var ig = dm.GetILGenerator();

			var entity = ig.DeclareLocal(returnType);


			var iMappedEntities = ig.DeclareLocal(typeof(List<IMappedEntity>));
			ig.Emit(OpCodes.Newobj, typeof(List<IMappedEntity>).GetConstructor(Type.EmptyTypes));
			ig.Emit(OpCodes.Stloc_S, iMappedEntities.LocalIndex);


			var a = (from index in Enumerable.Range(0, record.FieldCount)
			         let name = record.GetName(index)
			         let keyName = name.ToLower() == "id"
			                       	? ""
			                       	: name.EndsWith("_id", true, CultureInfo.InvariantCulture)
			                       	  	? name.Substring(0, name.Length - 3)
			                       	  	: name
			         let parent = keyName == ""
			                      	? "-"
			                      	: !keyName.Contains("_")
			                      	  	? ""
			                      	  	: keyName.Substring(0, keyName.LastIndexOf("_"))
			         select Tuple.Create(keyName,parent,index)).ToArray();

			var t = TreeNode.BuildTree(a, "");

			SetValues(t, entity, ig, record, iMappedEntities, key);

			ig.Emit(OpCodes.Ldloc_S, iMappedEntities);
			ig.Emit(OpCodes.Call, ExpressionUtility<object>.GetMethodInfo(x=>EntityGenerator.OnCreateMappedEntities(null)));

			ig.Emit(OpCodes.Ldloc_S, entity.LocalIndex);
			ig.Emit(OpCodes.Ret);

			return (Func<IDataRecord, TEntity, T>)dm.CreateDelegate(typeof(Func<IDataRecord, TEntity, T>));
		}

		private static void SetValues(TreeNode<string, int> treeNode, LocalBuilder entity, ILGenerator ig, IDataRecord record, LocalBuilder iMappedEntities, string key)
		{
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldc_I4, treeNode.Value);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<IDataRecord>.GetMethodInfo(x => x.IsDBNull(0)));
			var skipNested = ig.DefineLabel();
			ig.Emit(OpCodes.Brtrue, skipNested);

			ig.Emit(OpCodes.Call,
			        typeof (EntityGenerator<>).MakeGenericType(entity.LocalType).GetProperty("Create").GetGetMethod());
			ig.Emit(OpCodes.Call,
					typeof(Func<>).MakeGenericType(entity.LocalType).GetMethod("Invoke"));
			ig.Emit(OpCodes.Stloc_S, entity.LocalIndex);
			
			ig.Emit(OpCodes.Ldarg_1);
			var skipStepEntity = ig.DefineLabel();
			ig.Emit(OpCodes.Brfalse, skipStepEntity);
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Ldtoken, entity.LocalType);
			ig.Emit(OpCodes.Ldloc_S, entity.LocalIndex);
			ig.Emit(OpCodes.Call, ExpressionUtility<object>.GetMethodInfo(x => EntityGenerator.AddEntitesOfType(null, default(RuntimeTypeHandle), null)));
			ig.MarkLabel(skipStepEntity);

			if (typeof(IMappedEntity).IsAssignableFrom(entity.LocalType))
			{
				ig.Emit(OpCodes.Ldloc_S, iMappedEntities.LocalIndex);
				ig.Emit(OpCodes.Ldloc_S, entity.LocalIndex);
				ig.Emit(OpCodes.Call, ExpressionUtility<List<IMappedEntity>>.GetMethodInfo(x => x.Add(null)));
			}

			ig.Emit(OpCodes.Ldloc_S, entity);
			ig.Emit(OpCodes.Castclass, typeof(IFieldTracker));
			ig.Emit(OpCodes.Ldc_I4_1);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<IFieldTracker>.GetPropertyInfo(x => x.IsTrackingDisabled).GetSetMethod());

			ig.Emit(OpCodes.Ldloc_S, entity);
			ig.Emit(OpCodes.Castclass, typeof(IMapperKeyEntity));
			ig.Emit(OpCodes.Ldstr, key);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<IMapperKeyEntity>.GetMethodInfo(x => x.SetMapperKey(null)));

			SetValue(ig, record, treeNode.Value, entity, key);

			foreach (var node in treeNode.Children)
			{
				var name = node.Key;
				var type = entity.LocalType;
				var propertyName = !name.Contains("_") ? name : name.Substring(name.LastIndexOf("_") + 1);
				var propertyInfo = GetPropertyInfo(propertyName, type);
				if (propertyInfo == null)
				{
					var message = String.Format("Wrong property name {0} for type {1}. Please check Stored Procedure '{2}'", propertyName, type.Name, key);
					Trace.WriteLine(message);
					continue;
				}
				if (propertyInfo.PropertyType.IsSimpleType())
				{
					SetValue(ig, record, node.Value, entity, key);
				}
				else
				{
					var nestedEntity = ig.DeclareLocal(propertyInfo.PropertyType);
					SetValues(node, nestedEntity, ig, record, iMappedEntities, key);
					ig.Emit(OpCodes.Ldloc_S, entity);
					ig.Emit(OpCodes.Ldloc_S, nestedEntity);
					ig.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());
				}
			}

			ig.Emit(OpCodes.Ldloc_S, entity);
			ig.Emit(OpCodes.Castclass, typeof(IFieldTracker));
			ig.Emit(OpCodes.Ldc_I4_0);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<IFieldTracker>.GetPropertyInfo(x => x.IsTrackingDisabled).GetSetMethod());

			ig.MarkLabel(skipNested);
		}

		private static void SetValue(ILGenerator ig, IDataRecord record, int index, LocalBuilder entity, string key)
		{
			var name = record.GetName(index);
			var type = entity.LocalType;
			var fieldType = record.GetFieldType(index);

			var propertyName = !name.Contains("_") ? name : name.Substring(name.LastIndexOf("_") + 1);
			var propertyInfo = GetPropertyInfo(propertyName, type);


			#region SetPropertyValue

			if (propertyInfo.PropertyType != fieldType 
				&& Nullable.GetUnderlyingType(propertyInfo.PropertyType) != fieldType
				&& CheckEnum(fieldType, propertyInfo.PropertyType))
			{
				throw new Exception(
					String.Format("Wrong property {1}.{0} type {3}, expected {4}. Please check Stored Procedure '{2}'", propertyName,
					              type.Name, key, fieldType.Name, propertyInfo.PropertyType.Name));
			}

			ig.BeginExceptionBlock();
			ig.Emit(OpCodes.Ldloc_S, entity.LocalIndex);
			LoadValue(ig, index, propertyInfo.PropertyType, fieldType);
			ig.Emit(OpCodes.Callvirt, propertyInfo.GetSetMethod());

			ig.BeginCatchBlock(typeof (Exception));
			var e = ig.DeclareLocal(typeof (Exception));
			ig.Emit(OpCodes.Stloc_S, e);
			ig.Emit(OpCodes.Ldstr, String.Format(
				"Type: [{0}]; Property: [{1}]; Exception : {{0}} Please check Stored Procedure '{2}'",
				type.Name, propertyInfo.Name, key));
			ig.Emit(OpCodes.Ldloc_S, e);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<Exception>.GetPropertyInfo(x => x.Message).GetGetMethod());
			ig.Emit(OpCodes.Call, ExpressionUtility<String>.GetMethodInfo(x => String.Format("", new object())));
			ig.Emit(OpCodes.Ldloc_S, e);
			ig.Emit(OpCodes.Newobj, typeof (Exception).GetConstructor(new[] {typeof (String), typeof (Exception)}));
			ig.Emit(OpCodes.Throw);
			ig.EndExceptionBlock();

			#endregion
		}

	    private static bool CheckEnum(Type fieldType, Type propertyType)
	    {
	        var nullable = Nullable.GetUnderlyingType(propertyType);
	        bool isEnum = propertyType.IsEnum;
            if (nullable != null && nullable.IsEnum)
            {
                isEnum = true;
            }
            return (!isEnum || Enum.GetUnderlyingType(nullable ?? propertyType) != fieldType);
	    }

	    private static PropertyInfo GetPropertyInfo(string propertyName, Type type)
		{
			var propertyInfo = type.GetProperty(propertyName, 
				BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);

			if(propertyInfo == null && type.IsInterface)
			{
				foreach (var @interface in type.GetInterfaces())
				{
					propertyInfo = GetPropertyInfo(propertyName, @interface);
					if(propertyInfo != null)
					{
						return propertyInfo;
					}
				}
			}
			return propertyInfo;
		}

		private static void LoadValue(ILGenerator ig, int index, Type propertyType, Type fieldType)
		{
			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldc_I4, index);
			ig.Emit(OpCodes.Callvirt, ExpressionUtility<IDataRecord>.GetMethodInfo(x => x.IsDBNull(0)));
			var notNullLabel = ig.DefineLabel();
			var endIfLabel = ig.DefineLabel();
			ig.Emit(OpCodes.Brfalse_S, notNullLabel);
			if (propertyType.CanBeNull())
			{	
				ig.Emit(OpCodes.Ldnull);
				ig.Emit(OpCodes.Unbox_Any, propertyType);
			}
			else
			{
				ig.Emit(OpCodes.Ldstr, "Property cant be null.");
				ig.Emit(OpCodes.Newobj, typeof (InvalidCastException).GetConstructor(new[] {typeof (String)}));
				ig.Emit(OpCodes.Throw);
			}
			ig.Emit(OpCodes.Br_S, endIfLabel);
			ig.MarkLabel(notNullLabel);

			ig.Emit(OpCodes.Ldarg_0);
			ig.Emit(OpCodes.Ldc_I4, index);
			ig.Emit(OpCodes.Callvirt, GetMethods[fieldType]);
            if (Nullable.GetUnderlyingType(propertyType) == fieldType)
			{
				ig.Emit(OpCodes.Newobj, propertyType.GetConstructor(new[] {fieldType}));
			}
            else if (propertyType.IsNullable() && Nullable.GetUnderlyingType(propertyType).IsEnum && Enum.GetUnderlyingType(Nullable.GetUnderlyingType(propertyType)) == fieldType)
            {
                ig.Emit(OpCodes.Newobj, propertyType.GetConstructor(new[] { Nullable.GetUnderlyingType(propertyType) }));
            }
			ig.MarkLabel(endIfLabel);
		}

		public static T GetMulti(string key, IDataRecord record, int index, TEntity stepEntity, SqlMultiMapperSettings<TEntity> settings)
		{
		    var multiMapper = MultiMapperDelegates.GetOrAdd(key, e => CreateMultiMapperDelegate(key, record, index, settings));
            return multiMapper(record, stepEntity);
		}

		private static Func<IDataRecord, TEntity, T> CreateMultiMapperDelegate(string key, IDataRecord record, int index, SqlMultiMapperSettings<TEntity> settings)
		{
			Type returnType = typeof (TEntity);
			var dm = new DynamicMethod(String.Format("ʬʬMultiMapperDelegates_{0}", key), returnType, new[] { typeof(IDataRecord), typeof(TEntity) });
			var ig = dm.GetILGenerator();
			var stepEntity = ig.DeclareLocal(returnType);
			ig.Emit(OpCodes.Ldarg_1);
			ig.Emit(OpCodes.Stloc_S, stepEntity);

			Action<LocalBuilder> addEntitiesOfType = entity =>
			{
				ig.Emit(OpCodes.Ldloc_S, stepEntity);
				ig.Emit(OpCodes.Castclass, typeof(ILoadedEntityContainer));
				ig.Emit(OpCodes.Ldtoken, entity.LocalType);
				ig.Emit(OpCodes.Ldloc_S, entity);
				ig.Emit(OpCodes.Call, ExpressionUtility<object>.GetMethodInfo(x => EntityGenerator.AddEntitesOfType(null, default(RuntimeTypeHandle), null)));
			};

			if (index == 0 && settings.FirstSetIsEntity)
			{
				ig.Emit(OpCodes.Ldstr, key);
				ig.Emit(OpCodes.Ldarg_0);
				ig.Emit(OpCodes.Ldarg_1);
				ig.Emit(OpCodes.Call, ExpressionUtility<object>.GetMethodInfo(x=>Mappers<TEntity, TEntity>.Get(null, null, null)));
				ig.Emit(OpCodes.Stloc_S, stepEntity);
				addEntitiesOfType(stepEntity);
			}
			else
			{	
				var stepEntityNotNull = ig.DefineLabel();
				ig.Emit(OpCodes.Ldloc_S, stepEntity);
				ig.Emit(OpCodes.Brtrue_S, stepEntityNotNull);
				ig.Emit(OpCodes.Call, ExpressionUtility<object>.GetPropertyInfo(x => EntityGenerator<TEntity>.Create).GetGetMethod());
				ig.Emit(OpCodes.Call, ExpressionUtility<Func<TEntity>>.GetMethodInfo(x => x.Invoke()));
				ig.Emit(OpCodes.Stloc_S, stepEntity);
				addEntitiesOfType(stepEntity);
				ig.MarkLabel(stepEntityNotNull);

				var propertyMapping = settings.GetProperty(key);
				if (propertyMapping == null)
				{
					throw new ArgumentOutOfRangeException(
						string.Format("Mapper configuration for multi reader is not provded for {0}.", key));
				}

				var property = propertyMapping.Property;

				var elementType = !property.PropertyType.IsEnumerable()
				                  	? property.PropertyType
				                  	: property.PropertyType.GetGenericArguments()[0];

				var result = ig.DeclareLocal(elementType);
				if(elementType.IsSimpleType())
				{
					var fieldType = record.GetFieldType(0);
					if (elementType != fieldType && Nullable.GetUnderlyingType(elementType) != fieldType)
					{
						throw new Exception(
							String.Format("Wrong type {1}, expected {2}. Please check Stored Procedure '{0}'", key, fieldType.Name, elementType.Name));
					}
					LoadValue(ig, 0, elementType, fieldType);
				}
				else
				{
					ig.Emit(OpCodes.Ldstr, key);
					ig.Emit(OpCodes.Ldarg_0);
					ig.Emit(OpCodes.Ldloc_S, stepEntity);
					ig.Emit(OpCodes.Call, typeof(Mappers<,>).MakeGenericType(elementType, typeof(TEntity)).GetMethod("Get"));
				}
				ig.Emit(OpCodes.Stloc_S, result);

				ig.Emit(OpCodes.Ldloc_S, stepEntity);
				ig.Emit(OpCodes.Castclass, typeof(ILoadedEntityContainer));
				ig.Emit(OpCodes.Ldtoken, propertyMapping.ListEntityType);
				ig.Emit(OpCodes.Callvirt, ExpressionUtility<ILoadedEntityContainer>.GetMethodInfo(x=>x.GetLoadedEntities(default(RuntimeTypeHandle))));
				var data = ig.DeclareLocal(typeof(List<object>));
				ig.Emit(OpCodes.Stloc_S, data);
				var i = ig.DeclareLocal(typeof (int));
				var dataCount = ig.DeclareLocal(typeof (int));
				ig.Emit(OpCodes.Ldloc_S, data);
				ig.Emit(OpCodes.Call, ExpressionUtility<List<object>>.GetPropertyInfo(x=>x.Count).GetGetMethod());
				ig.Emit(OpCodes.Stloc_S, dataCount);
				var entity = ig.DeclareLocal(propertyMapping.ListEntityType);

				var begin = ig.DefineLabel();
				var end = ig.DefineLabel();
				ig.Emit(OpCodes.Br, end);
				ig.MarkLabel(begin);

				ig.Emit(OpCodes.Ldloc_S, data);
				ig.Emit(OpCodes.Ldloc_S, i);
				ig.Emit(OpCodes.Call, ExpressionUtility<List<object>>.GetMethodInfo(x=>x[0]));
				ig.Emit(OpCodes.Castclass, propertyMapping.ListEntityType);
				ig.Emit(OpCodes.Stloc_S, entity);
				ig.Emit(OpCodes.Ldloc_S, i);
				ig.Emit(OpCodes.Ldc_I4_1);
				ig.Emit(OpCodes.Add);
				ig.Emit(OpCodes.Stloc_S, i);

				ig.Emit(OpCodes.Ldloc_S, result);
				ig.Emit(OpCodes.Ldloc_S, entity);
				ig.Emit(OpCodes.Ldnull);


				ig.Emit(OpCodes.Call, propertyMapping.Matching);
				ig.Emit(OpCodes.Brfalse, end);

				SetProperty(entity, result, property, ig);
				if (propertyMapping.BackProperty != null)
				{
					SetProperty(result, entity, propertyMapping.BackProperty, ig);
				}

				ig.MarkLabel(end);
				ig.Emit(OpCodes.Ldloc_S, i);
				ig.Emit(OpCodes.Ldloc_S, dataCount);
				ig.Emit(OpCodes.Clt);
				ig.Emit(OpCodes.Brtrue, begin);
			}
			ig.Emit(OpCodes.Ldloc_S, stepEntity);
			ig.Emit(OpCodes.Ret);
			return (Func<IDataRecord, TEntity, T>)dm.CreateDelegate(typeof(Func<IDataRecord, TEntity, T>));
		}

		private static void SetProperty(LocalBuilder entity, LocalBuilder value, PropertyInfo property, ILGenerator ig)
		{
			ig.Emit(OpCodes.Ldloc_S, entity);
			if (!property.PropertyType.IsEnumerable())
			{
				ig.Emit(OpCodes.Ldloc_S, value);
				ig.Emit(OpCodes.Callvirt, property.GetSetMethod());
			}
			else
			{
				ig.Emit(OpCodes.Callvirt, property.GetGetMethod());
				ig.Emit(OpCodes.Castclass, typeof(IList));
				ig.Emit(OpCodes.Ldloc_S, value);
				ig.Emit(OpCodes.Callvirt, ExpressionUtility<IList>.GetMethodInfo(x=>x.Add(null)));
				ig.Emit(OpCodes.Pop);
			}
		}
	}
}